#!/usr/bin/python
import os
import sys # command line args
from PIL import Image
from PIL.ExifTags import TAGS
import shutil # copy a file
import time
import datetime
#import glob # thumbnails
#import subprocess # running programs (used to display thumbnails with xv)
import pickle #save objects to disk (preferences)
import filecmp
import fnmatch
from PyQt4.QtGui import QImage, QTransform, QValidator

#python debugger.
# use 'pdb.set_trace()' in the code to start the debugger
# s = step
# n = next
# q = quit
import pdb #python debugger.

def defaultPrefs():
	prefs = {
	"photoLibraryPath" : os.path.expanduser("~/Pictures/"),
	"sourceDir" : os.path.expanduser("~/Pictures/in"),
	"monthDirFormat" : "%m %B",
	"photoExtensions" : ("jpg", "raw"),
	"videoExtensions" : ("avi", "mp4", "mpg"),
	"importPhotos": True,
	"importVideos": True,
	"deleteMedia" : False,
	"rename" : False,
	"renameString" : "",
	"renameStringNum" : 1,  # suffix number. ex: familyPhoto_001.jpg
	"renameStringNumPadding" : 3,
	"renameStringNumPaddingSeparator": "_",
	"prefsSavePath" : os.path.join(os.path.expanduser("~/.openshutterspeed"), "preferences.pref")
	}
	return prefs

# Global Preferences:
prefs = defaultPrefs()

# Global Variables:
#Set what percent each task will take. Should be less than 1.0 to allow
#deletefiles and wrap up time at the end
TASK_WEIGHT_MOVEFILES = .95 
TASK_WEIGHT_FINDFILES = .025

QUIT_IMPORT = False
QUIT_THUMBS = False
RESTART_THUMBS = False
FILE_BROWSE_RUNNING = False

LINUX_MOUNT_POINTS = [ os.path.expanduser("~/.gvfs"), "/media" ]
GROUP_SUBDIR_NAME = None
FILE_GROUP_SIZE = 50
MAX_FILES_IN_THUMB_BROWSER = 1000

#args: path of dir
#return: list of files to copy
def findFiles(rootdir, callback_import_progress=None, callback_files=None, justOne=False, incremental=False):
	extensions = ()
	if prefs["importPhotos"]:
		extensions += prefs["photoExtensions"]
	if prefs["importVideos"]:
		extensions += prefs["videoExtensions"]

	if callback_import_progress:
		callback_import_progress.emit("Finding photos to import", None, TOTAL_PROGRESS)
	fileList = []
	groupIndex = 0
	for root, subFolders, files in os.walk(rootdir, followlinks=False):
		if os.path.basename(os.path.normpath(root))[0] == '.':
			subFolders[:] = []  #skip all subdirectories with a hidden parent
			continue #skip hidden directories
		for file in files:
			if QUIT_THUMBS or QUIT_IMPORT or RESTART_THUMBS:
				return None
			if file[0] == '.':
				continue #skip hidden files
			if file.lower().endswith(extensions):
				fileList.append(os.path.join(root,file))
				if justOne:
					return fileList
				if callback_files:
					currentCount = len(fileList)
					if not incremental:
						if currentCount >= MAX_FILES_IN_THUMB_BROWSER:
							return fileList
					else: #send back increments of files found
						if groupIndex+FILE_GROUP_SIZE == currentCount:
							if currentCount >= MAX_FILES_IN_THUMB_BROWSER:
								callback_files.emit(fileList[groupIndex:],True)
								return fileList
							callback_files.emit(fileList[groupIndex:], False)
							groupIndex += FILE_GROUP_SIZE

	if callback_files and incremental:
		callback_files.emit(fileList[groupIndex:], True)
	return fileList

#args: list of files (full path)
#return: list of files successfully copied
def moveFiles(fileList, callback_import_progress=None):
	global TOTAL_PROGRESS
	movedFiles = []
	duplicateFiles = []
	errorFiles = []
	renameIndex = None 
	groupDir = None
	sizeDict = {}  # Store file sizes of dest directories	

	if fileList is None:
		return None, None, None

	for item in fileList:
		if QUIT_IMPORT:
			return None, None, None
		
		itemDate, usedExif = getDate(item)
		if not itemDate: # file is skipped if date cannot be found
			errorFiles.append(item)
			continue 

		destDir = destPath(prefs,itemDate,GROUP_SUBDIR_NAME,groupDir)
		if GROUP_SUBDIR_NAME is not None and groupDir is None:
			groupDir = destDir

		destExists = os.path.exists(destDir)	
		if renameIndex is None:
			renameIndex = getRenameFirstIndex(destDir, destExists)
		
		filename = generateFileName(item, prefs, renameIndex) # 'name.ext'
		if filename is None: 
			# file is skipped if error reading file 
			# (specifically having no file extention)
			errorFiles.append(item)
			continue
			
		if not destExists:
			try:
				os.makedirs(destDir)
			except:
				print "Failed to create directory ", destDir
				continue
		else:
			filename = avoidDuplicateNames(filename, item, destDir)
			if filename is None:
				duplicateFiles.append(item)
				continue

		destFilepath = os.path.join(destDir,filename)
		if callback_import_progress:
			thumb_pixbuf = thumb(item, width=300, height=256, autoRotate=False, useQt=True)
			TOTAL_PROGRESS += TASK_WEIGHT_MOVEFILES/len(fileList)
			callback_import_progress.emit("Importing: "+destFilepath, thumb_pixbuf, TOTAL_PROGRESS)

		if not destDir in sizeDict:	
			#Need to add file sizes to dictionary for this directory.
			#Used to detect duplicates with different names
			sizeDict[destDir] = {}
			dFiles = os.listdir(destDir)
			for f in dFiles:
				fullf = os.path.join(destDir,f)
				size=os.path.getsize(fullf)
				if not size in sizeDict[destDir]:
					sizeDict[destDir][size] = []
				sizeDict[destDir][size].append(fullf)

		#Check for duplicate image, different name
		s = os.path.getsize(item)
		if s in sizeDict[destDir]:
			dup = False
			#File size matched at least 1, now do a full binary compare
			for fileSameSize in sizeDict[destDir][s]:
				if filecmp.cmp(item, fileSameSize, shallow=False):
					print "Exact duplicate found named", fileSameSize 
					duplicateFiles.append(item)
					dup = True
					break
			if dup:
				continue

		try:
			shutil.copy2(item, destFilepath)
		except:
			#print "error copying file, it will NOT be erased"
			errorFiles.append(item)
			continue

		if not s in sizeDict[destDir]:
			sizeDict[destDir][s] = []
		sizeDict[destDir][s].append(destFilepath)
		movedFiles.append(item)
		renameIndex += 1
	renameIndex = None # reset counter when all files are copied
	return movedFiles, duplicateFiles, errorFiles

def deleteFiles(fileList):
	errorFiles = []
	for item in fileList:
		if QUIT_IMPORT:
			return
		
		name_basename, extension = os.path.splitext(item)
		thumbPath = None
		try:
			if os.path.isfile(name_basename+".THM"):
				thumbPath = name_basename+'.THM'
			elif os.path.isfile(name_basename+".thm"):
				thumbPath = name_basename+'.thm'

			os.remove(item)
			print "deleted", item,
			
			if thumbPath and os.path.getsize(thumbPath) < 1024 * 500:
				os.remove(thumbPath)
				print "deleted", thumbPath
							
		except OSError:
			print "error moving", item
			errorFiles.append(item)
	return errorFiles


def getRenameFirstIndex(destDir, destExists):
	if not prefs["rename"] or destExists is False:
		return prefs["renameStringNum"]

	numpad = prefs["renameStringNumPadding"]
	# String of ?'s corresponding to padding length
	paddedQs="".join(['?']*numpad)

	pattern = prefs["renameString"]+prefs["renameStringNumPaddingSeparator"]+paddedQs+".???"

	files = os.listdir(destDir)
	matchingFiles = fnmatch.filter(files, pattern)

	while len(matchingFiles) > 0:
		highest = max(matchingFiles)
		name = os.path.splitext(highest)[0]
		indexStr = name[-numpad:]
		try:
			return int(indexStr)+1
		except:
			matchingFiles.remove(highest)
			
	return prefs["renameStringNum"] # not found, use default

# first tries to get the meta data, if that fails, it will try the date modified,
#  if that fails, it will return None
# returns a date obj or None
def getDate(item, exif=None):
	dateObj = None
	usedExif = False
	if exif is None: exif = getEXIF(item)

	if exif is not None and 'DateTimeOriginal' in exif:
		dateString = exif['DateTimeOriginal']
		try: 
			dateObj=datetime.datetime.strptime(dateString, "%Y:%m:%d %H:%M:%S") 
			usedExif = True
		except: dateObj=None
	
	if dateObj is None:	
		#using date modified from file
		try:
			dateSeconds = os.path.getmtime(item)
			dateString = time.localtime(dateSeconds) # convert seconds to date
			dateString = time.strftime("%x, %X", dateString) # format to MM/DD/YYYY
			dateObj = datetime.datetime.strptime(dateString,"%x, %X") 
		except: dateObj = None

	return dateObj, usedExif 

def getDateSeconds(imagePath):
	dateObj, usedExif = getDate(imagePath)
	return time.mktime(dateObj.timetuple()) #epoch time

def getEXIF(imagepath):
	try: #reading EXIF data
		exif = {}
		i = Image.open(imagepath)
		info = i._getexif()
		for tag, value in info.items():
			decoded = TAGS.get(tag, tag)
			exif[decoded] = value
		return exif
	except:
		return None

# takes in a date and returns destination path as a string
def destPath(lprefs, date=datetime.date.today(), subdir=None, groupDir=None):
	if groupDir is not None:
		return groupDir

	monthDir = os.path.join(lprefs["photoLibraryPath"], date.strftime("%Y"), date.strftime(lprefs["monthDirFormat"]))

	if subdir is not None:
		monthDir = os.path.join(monthDir, subdir)

	return monthDir

# returns either original filename or a custom filename if prefs["rename"]==True (with correct number padding)
def generateFileName(srcFilePath, lprefs, num=None):
	try: #gets original filename and ext
		filename = os.path.split(srcFilePath)[1] # 'name.ext'
		(name, ext) = os.path.splitext(filename)# 'name', '.ext'
	except:
		print "could not read file:", path
		return None
	
	if lprefs["rename"] and lprefs["renameString"].strip() != "" and num !=None:
		numberBuffer = str(num).zfill(lprefs["renameStringNumPadding"])
		name = lprefs["renameString"] + lprefs["renameStringNumPaddingSeparator"] + numberBuffer

	return name + ext.lower()

def avoidDuplicateNames(filename, srcFilePath, destDir):
	#Check if a file with this name already exists
	loop = True
	while loop:
		loop = False
		destFilePath = os.path.join(destDir, filename)
		if os.path.isfile(destFilePath):
			if filecmp.cmp(srcFilePath, destFilePath, shallow=False):
				print "Exact duplicate found"
				return None
			else:
				(name, ext) = os.path.splitext(filename)
				filename=name+"a"+ext #append to change the name 
				loop = True #make sure this new name isn't taken
	return filename

def thumb(path, width=256, height = None, autoRotate=True, useQt=False):
	if not height:height = width
	
	# check if thumbnail was created by the camera for video files
	thumb_basename, extension = os.path.splitext(str(path))
		
	if os.path.isfile(thumb_basename+".THM"):
		thumbPath = thumb_basename+'.THM'
	elif os.path.isfile(thumb_basename+".thm"):
		thumbPath = thumb_basename+'.thm'
	else:
		thumbPath = path

	# Don't do large files
	if os.path.getsize(thumbPath) > 1024 * 1024 * 15 :
		return None

	try:
		if useQt:
			image = QImage(thumbPath)
			if image.isNull():
				image = None
			else:
				image = image.scaled(width,height, aspectRatioMode=1)
		else:
			image = Image.open(thumbPath)
			image.thumbnail((width,height), Image.ANTIALIAS)
	except:
		return None
	if autoRotate:
		exif = getEXIF(thumbPath)
		if exif is not None and 'Orientation' in exif and exif['Orientation'] !=1:
			degrees = rotateImage(exif['Orientation'])	
			if degrees != 0: 
				if useQt:	
					t = QTransform()
					t.rotate(-degrees)
					image = image.transformed(t)
				else:
					image = image.rotate(degrees, expand=True)	
	return image 

#image is of type Image from Python Imaging Library (PIL)
def rotateImage(orient):
	'''
	Orientation
  	1: 'Horizontal (normal)',
   2: 'Mirrored horizontal',
   3: 'Rotated 180',
   4: 'Mirrored vertical',
   5: 'Mirrored horizontal then rotated 90 CCW',
   6: 'Rotated 90 CW',
   7: 'Mirrored horizontal then rotated 90 CW',
   8: 'Rotated 90 CCW'})
	'''
	if orient==3:
		rotation=180
	elif orient==6:
		rotation=270
	elif orient==8:
		rotation=90
	else:
		rotation=0

	return rotation

# Convert between string or tuple (used with extention preferences)
def stringTuple(userInput):
	if type(userInput) is tuple:
		return str(userInput)[1:-1].replace("'","") #removes tuple formatting
	if type(userInput) is str:
		userInput = userInput.replace(" ", ",") #spaces to commas. This allows user to make the list space OR comma separated
		userInput = userInput.replace(".", ",")
		stringList = userInput.split(",")
		stringList = filter(bool, stringList) #remove empty entries from the list
		return tuple(stringList)
	print "Bad preference Input: Var type:", type(userInput)
	return None # bad input from user
		
def verifyPaths():
	global prefs
	listOfPaths = { "photoLibraryPath" : prefs["photoLibraryPath"], "sourceDir" : prefs["sourceDir"] }
	for p in listOfPaths:
		lastpath = None
		newpath = listOfPaths[p]
		while not lastpath is newpath:
			if os.path.isdir(newpath):
				prefs[p] = newpath
				break
			else:
				lastpath = newpath
				newpath = os.path.normpath(os.path.join(newpath, ".."))

# Primary method for initializing preferences
# loads prefs from disk or loads defaults if nothing is on disk
def loadPrefs():
	global prefs
	prefFile = prefs["prefsSavePath"] 

	if os.path.isfile(prefFile):
		try:
			savedFile = open(prefFile, 'r+') 
			loadedprefs = pickle.load(savedFile)
			for name in loadedprefs:
				if name in prefs:
					prefs[name] = loadedprefs[name]
				else:
					print "Throwing away old preference: ", name
			savedFile.close()
		except:
			print "Failed to load existing preferences. Using defaults"
			try:
				os.remove(prefFile)
			except OSError:
				print "Error cleaning up preference file"

	verifyPaths()

	
def savePrefs():
	saveDir = os.path.split(os.path.normpath(prefs["prefsSavePath"]))[0]
	try:
		if not os.path.exists(saveDir):
	 		os.makedirs(saveDir)
	except:
		print "Could not make saved files, check if you have permissions"
		return False
	
	try:
		savedFile = open(prefs["prefsSavePath"], 'w')
		pickle.dump(prefs, savedFile)
		savedFile.close()
	except IOError:
		return False
	return True # file saved correctly

def findCamera():
	global prefs
	D=None
	for p in LINUX_MOUNT_POINTS:
		if not os.path.isdir(p):
			continue
		for mount in os.listdir(os.path.realpath(p)):
			mountPath = os.path.join(p, mount)
			if os.path.ismount(mountPath):
				dirlist = os.listdir(mountPath)
				if "DCIM" in dirlist:
					D="DCIM"
				elif "dcim" in dirlist:
					D="dcim"
				if D is not None:
					if "Android" in dirlist:
						prefs["sourceDir"] = os.path.join(mountPath,D)
					else:
						prefs["sourceDir"] = mountPath
					print "Camera at: ", prefs["sourceDir"]
					return
	
	
def Import(cb_progress=None, cb_finished=None, selected_files=None):
	global QUIT_IMPORT
	global TOTAL_PROGRESS
	global prefs
	
	TOTAL_PROGRESS = 0.005
	if selected_files is not None and len(selected_files) > 0:
		files = selected_files
	else:
		files = findFiles(prefs["sourceDir"], callback_import_progress=cb_progress)
	TOTAL_PROGRESS = TASK_WEIGHT_FINDFILES

	if files is not None: 
		(movedFiles, duplicateFiles, errorFiles) = moveFiles(files, callback_import_progress=cb_progress)

	errorDeleteFiles = None
	if prefs["deleteMedia"] and not QUIT_IMPORT:
		if cb_progress:
			cb_progress.emit("Deleting original files", None, .97)
		errorDeleteFiles = deleteFiles(movedFiles)

	if cb_finished and not QUIT_IMPORT:
		errorAndWarnings = ""
		if len(errorFiles) > 0:
			errorAndWarnings += "Errors:\n" + "\n".join(errorFiles) + "\n\n"
		if len(duplicateFiles) > 0:
			errorAndWarnings += "Duplicates Not Copied:\n" + "\n".join(duplicateFiles)
		if errorDeleteFiles and len(errorDeleteFiles) > 0:
			errorAndWarnings += "Unable to delete original:\n" + "\n".join(errorDeleteFiles)
		cb_finished.emit(len(movedFiles), errorAndWarnings) 
	QUIT_IMPORT=False


def getBrowserFiles(restartCount, cb_file, callback_thumb_file, sortByDate=True):
	global QUIT_THUMBS
	global RESTART_THUMBS
	global FILE_BROWSE_RUNNING
	#print "Backend: Loading thumbs from ", prefs["sourceDir"]
	filelist = findFiles(prefs["sourceDir"], callback_files=cb_file, incremental= not sortByDate)
	if filelist is not None:
		if sortByDate:
			filelist.sort(key=getDateSeconds)
			cb_file.emit(filelist, True)	

		index = 0
		for path in filelist:
			if QUIT_THUMBS:
				QUIT_THUMBS = False
				return
			name = os.path.split(path)[1] # 'name.ext'
			icon = thumb(path, width=100, height=100, useQt=True)

			if RESTART_THUMBS:
				break 
			else:
				callback_thumb_file.emit(restartCount, path, name, icon, index)
			index += 1

	if RESTART_THUMBS:
		RESTART_THUMBS = False
		#print "Restarting loading thumbs"
		getBrowserFiles(restartCount+1,cb_file, callback_thumb_file)		
	QUIT_THUMBS = False
	FILE_BROWSE_RUNNING = False

def getThumbAndInfo(filepath, cb_info, cb_thumb, maxWidth=300, maxHeight=256):
	exif = getEXIF(filepath)
	d, usedExif = getDate(filepath, exif)
	if usedExif:
		metaStr = "Photo Taken: " 
	else:
		metaStr = "Last modified: " 
	
	if d: metaStr += d.strftime("%b")+" "+d.strftime("%d")+", "+d.strftime("%Y")+" " + d.strftime("%X")


	fileSizeMB = "%.3f" % (os.path.getsize(filepath)/(1024.0*1024.0))
	metaStr += "\nFile size: " + fileSizeMB + "MB"

	if exif is not None and 'Model' in exif:
		metaStr += "\n"+ exif['Model'].rstrip('\0')
	
	cb_info.emit(metaStr)

	t = thumb(filepath, width=maxWidth, height=maxHeight, useQt=True)
	cb_thumb.emit(t)
	

if __name__ == "__main__": # If this is run as a stand-alone app
	print #empty line
	if (len(sys.argv) > 1):
		if os.path.exists(sys.argv[1]):
			prefs["sourceDir"] = sys.argv[1]
		else:
			print "Invalid source directory"
			exit()
	print "Using source directory: ", prefs["sourceDir"]
	print "Running camera_import_backend..." 
	Import()
	print "Complete."
else: # this is imported into another python script (GUI)
	loadPrefs()


class CharValidator(QValidator):
	def validate(self, string, integer):
		if self.hasBadChars(string):
			return QValidator.Invalid, integer	
		return QValidator.Acceptable, integer
	
	def hasBadChars(self,  string):
		badchars = '\/:*?^"<>|'
		for c in badchars:
			if string.contains(c):
				return True

class FileNameValidator(CharValidator):
	def validate(self, string, integer):
		if self.hasBadChars(string):
			return QValidator.Invalid, integer
		if string.startsWith(" "): return QValidator.Invalid, integer
		if string.endsWith(" ") or string.endsWith("."): return QValidator.Intermediate,  integer
		return QValidator.Acceptable, integer 
	
	def fixup(self,  string):
		fixedStr = string.trimmed()
		while fixedStr.endsWith(".") or fixedStr.endsWith(" "):
			fixedStr.chop(1)
		string.clear()
		string.append(fixedStr)
		return

#TODO: Currently not working. Need to come up with a "validate" algorithm that only accepts strings formatted "jpg, raw"
#			and then autocorrects improperly formatted strings
class PrefsExtensionValidator(CharValidator):
	def validate(self, string, integer):
		if self.hasBadChars(string):
			return QValidator.Invalid, integer
		if string.contains("."): return QValidator.Invalid,  integer	
		self.fixup(string)		
		return QValidator.Acceptable, integer 
	
	def fixup(self,  string):
		fixedStr = str(string)
		fixedStr = fixedStr.replace(",", " ") #commas to spaces. This allows user to make the list space OR comma separated
		stringList = fixedStr.split()
		fixedStr = ', '.join(map(str, stringList))
		string.clear()
		string.append(fixedStr)
		return	
		
